package net.towee.client.managers.impl;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;

import net.towee.client.event.core.GroupCreatedEvent;
import net.towee.client.event.core.GroupUpdatedEvent;
import net.towee.client.event.core.UserEnteredToGroupEvent;
import net.towee.client.event.core.UserEnteredToGroupHandler;
import net.towee.client.event.core.UserLeaveFromGroupEvent;
import net.towee.client.event.core.UserLeaveFromGroupHandler;
import net.towee.client.managers.AccountHelper;
import net.towee.client.managers.GroupsManager;
import net.towee.model.account.AccountGroup;
import net.towee.model.account.AccountIdentity;
import net.towee.model.account.AccountPermission;
import net.towee.model.group.GroupData;
import net.towee.model.group.GroupEnterRequest;
import net.towee.model.group.GroupIdentity;
import net.towee.model.group.GroupProfile;
import net.towee.model.utils.Pair;
import net.towee.server.rpc.services.ToweeServiceAsync;
import net.towee.shared.exceptions.AnswerIsWrong;

import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import com.google.common.collect.MapMaker;
import com.google.gwt.event.shared.HandlerManager;
import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.google.inject.Inject;

public class GroupsManagerImpl implements GroupsManager {
	private final Map<String, GroupData> dataCache;
	private final Map<String, GroupIdentity> identityCache;
	private final Map<String, GroupProfile> profileCache;
	private final Map<String, List<GroupProfile>> searchCache;

	private final ToweeServiceAsync service;
	private final HandlerManager eventBus;
	private final AccountHelper accountHelper;

	@Inject
	public GroupsManagerImpl(ToweeServiceAsync service, HandlerManager eventBus,
			AccountHelper accountHelper) {
		this.service = service;
		this.eventBus = eventBus;
		this.accountHelper = accountHelper;

		this.dataCache = new MapMaker().maximumSize(20).makeMap();

		this.identityCache = new MapMaker().maximumSize(100).makeMap();

		this.profileCache = new MapMaker().maximumSize(100).makeMap();

		this.searchCache = new MapMaker().maximumSize(40).expireAfterWrite(10, TimeUnit.MINUTES)
				.makeMap();

		createPushHandler();
	}

	private void createPushHandler() {
		service.onUserLeaveFromGroup(null, null, new AsyncCallback<GroupIdentity>() {
			@Override
			public void onFailure(Throwable caught) {
			}

			@Override
			public void onSuccess(GroupIdentity result) {
				// Remove group from account
				accountHelper.getAccount().getGroups().removeFromGroup(result.getId());

				// Fire an event
				eventBus.fireEvent(new UserLeaveFromGroupEvent(result));
			}
		});

		service.onUserEnterToGroup(null, null, new AsyncCallback<AccountGroup>() {
			@Override
			public void onFailure(Throwable caught) {
			}

			@Override
			public void onSuccess(AccountGroup result) {
				// Add group to account
				accountHelper.getAccount().getGroups().getGroupsList().add(result);

				// Fire an event
				eventBus.fireEvent(new UserEnteredToGroupEvent(result));
			}
		});
		
		eventBus.addHandler(UserLeaveFromGroupEvent.TYPE, new UserLeaveFromGroupHandler(){
			@Override
			public void onUserLeaveGroup(GroupIdentity identity) {
				if(dataCache.containsKey(identity.getId())) 
					dataCache.remove( identity.getId() );
			}
		});
	}

	@Override
	public void getGroupData(String id, final AsyncCallback<GroupData> handler) {
		Preconditions.checkArgument(!Strings.isNullOrEmpty(id), "null group id");

		if (dataCache.containsKey(id))
			handler.onSuccess(dataCache.get(id));
		else
			service.getGroupData(id, new AsyncCallback<GroupData>() {
				@Override
				public void onFailure(Throwable caught) {
					handler.onFailure(caught);
				}

				@Override
				public void onSuccess(GroupData result) {
					// Put to caches
					identityCache.put(result.getId(), result.getIdentity());
					dataCache.put(result.getId(), result);

					// Invoke handler
					handler.onSuccess(result);
				}
			});
	}

	@Override
	public void findGroup(String str, int offset, int limit,
			final AsyncCallback<List<GroupProfile>> handler) {
		Preconditions.checkArgument( !Strings.isNullOrEmpty(str), "null search string" );
		
		final String reqHash = str + offset + limit;

		if (searchCache.containsKey(reqHash))
			handler.onSuccess(searchCache.get(reqHash));
		else
			service.findGroup(str, offset, limit, new AsyncCallback<List<GroupProfile>>() {

				@Override
				public void onFailure(Throwable caught) {
					handler.onFailure(caught);
				}

				@Override
				public void onSuccess(List<GroupProfile> result) {
					// Put to cache
					searchCache.put(reqHash, result);

					// Invoke handler
					handler.onSuccess(result);
				}
			});
	}
	
	@Override
	public void getAllGroups(int offset, int limit, final AsyncCallback<List<GroupProfile>> handler) {
		final String reqHash = offset + limit + "ALL";

		if (searchCache.containsKey(reqHash))
			handler.onSuccess(searchCache.get(reqHash));
		else
			service.getAllGroup(offset, limit, new AsyncCallback<List<GroupProfile>>() {

				@Override
				public void onFailure(Throwable caught) {
					handler.onFailure(caught);
				}

				@Override
				public void onSuccess(List<GroupProfile> result) {
					// Put to cache
					searchCache.put(reqHash, result);

					// Invoke handler
					handler.onSuccess(result);
				}
			});
	}

	@Override
	public void whosOnline(String groupId, int offset, int limit,
			AsyncCallback<List<AccountIdentity>> handler) {
		service.whosOnline(groupId, offset, limit, handler);
	}

	@Override
	public void getGroupsByIds(List<String> ids, final AsyncCallback<List<GroupProfile>> handler) {
		final List<GroupProfile> result = new ArrayList<GroupProfile>();

		Iterator<String> it = ids.iterator();
		while (it.hasNext()) {
			String id = it.next();

			if (profileCache.containsKey(id)) {
				result.add(profileCache.get(id));
				it.remove();
			}
		}

		if (ids.size() == 0)
			handler.onSuccess(result);
		else
			service.getGroupsByIds(ids, new AsyncCallback<List<GroupProfile>>() {
				@Override
				public void onFailure(Throwable caught) {
					handler.onFailure(caught);
				}

				@Override
				public void onSuccess(List<GroupProfile> r) {
					Preconditions.checkNotNull(r, "fail get groups");

					// Put to cache
					for (GroupProfile g : r) {
						profileCache.put(g.getId(), g);
						identityCache.put(g.getId(), g.getIdentity());
					}

					// Create result array and invoke handler
					result.addAll(r);
					handler.onSuccess(result);
				}

			});
	}

	@Override
	public void addEnterRequest(String groupId, String accountId, String testAnsw,
			AsyncCallback<Boolean> handler) {
		service.addEnterRequest(groupId, accountId, testAnsw, handler);
	}

	@Override
	public void processEnterRequest(String groupId, String accountId, AsyncCallback<Boolean> handler) {
		// Check permissions
		Preconditions.checkArgument(
				accountHelper.hasPermissions(groupId, AccountPermission.CAN_PROCESS_ENTER_REQUESTS),
				"no permission");

		// Process request
		service.processEnterRequest(groupId, accountId, handler);
	}

	@Override
	public void getEnterRequests(String groupId, int offset, int limit, AsyncCallback<List<GroupEnterRequest>> handler) {
		// Check permissions
		Preconditions.checkArgument(accountHelper.hasPermissions(groupId,
				AccountPermission.CAN_PROCESS_ENTER_REQUESTS));

		service.getEnterRequests(groupId,  offset, limit, handler);
	}

	@Override
	public void createGroup(GroupData group, final AsyncCallback<GroupData> handler) {
		Preconditions.checkNotNull(group, "null group");
		Preconditions.checkNotNull(group.getIdentity(), "null group identity");
		Preconditions.checkArgument(!Strings.isNullOrEmpty(group.getIdentity().getPrintName()),
				"null group print name");
		Preconditions.checkArgument(!Strings.isNullOrEmpty(group.getIdentity().getAlias()),
				"null group alias");
		Preconditions
				.checkArgument(!Strings.isNullOrEmpty(group.getTestQuestion()), "null test question");

		service.createGroup(group, new AsyncCallback<GroupData>() {
			@Override
			public void onFailure(Throwable caught) {
				handler.onFailure(caught);
			}

			@Override
			public void onSuccess(GroupData result) {
				Preconditions.checkNotNull(result, "fail create group");

				// Add group to cache
				dataCache.put(result.getId(), result);
				identityCache.put(result.getId(), result.getIdentity());

				// Enter local account to group
				// Zero role is role for owners
				AccountGroup ag = accountHelper.getAccount().getGroups()
						.enterToGroup(result.getIdentity(), result.getRoleContainer().getRole(0));

				// Fire an events
				eventBus.fireEvent(new GroupCreatedEvent(result));
				eventBus.fireEvent(new UserEnteredToGroupEvent(ag));

				// Clear search cache
				searchCache.clear();

				// Invoke handler
				handler.onSuccess(result);
			}
		});
	}

	@Override
	public void updatePrintName(final String groupId, final String newName,
			final AsyncCallback<Boolean> handler) {
		service.updatePrintName(groupId, newName, new AsyncCallback<Boolean>() {
			@Override
			public void onFailure(Throwable caught) {
				handler.onFailure(caught);
			}

			@Override
			public void onSuccess(Boolean result) {
				Preconditions.checkArgument(result, "fail update group print name");

				// Update loacal cache and fire an event
				GroupData group = dataCache.get(groupId);
				if (group != null) {
					group.getIdentity().setPrintName(newName);
					eventBus.fireEvent(new GroupUpdatedEvent(group));
				}
			}

		});
	}

	@Override
	public void updateProfile(final GroupProfile groupProfile, final AsyncCallback<Boolean> handler) {
		service.updateGroupProfile(groupProfile, new AsyncCallback<Boolean>() {
			@Override
			public void onFailure(Throwable caught) {
				handler.onFailure(caught);
			}

			@Override
			public void onSuccess(Boolean result) {
				Preconditions.checkArgument(result, "fail update group profile");

				// Update loacal cache and fire an event
				GroupData group = dataCache.get(groupProfile.getId());
				if (group != null) {
					profileCache.put(groupProfile.getId(), groupProfile);
					eventBus.fireEvent(new GroupUpdatedEvent(group));
				}
			}

		});
	}

	@Override
	public void enterToGroup(String groupId, String testAnswer, final AsyncCallback<AccountGroup> handler) {
		Preconditions.checkArgument(!Strings.isNullOrEmpty(groupId), "null group id");
		Preconditions.checkArgument(!Strings.isNullOrEmpty(testAnswer), "null test answer");
		Preconditions.checkArgument(!accountHelper.isMemeberOfGroups(groupId), "already in group");

		service.enterToGroup(groupId, testAnswer, new AsyncCallback<AccountGroup>() {
			@Override
			public void onFailure(Throwable caught) {
				handler.onFailure(caught);
			}

			@Override
			public void onSuccess(AccountGroup result) {
				if (result == null)
					handler.onFailure(new AnswerIsWrong());
				else {
					// Add group to local stored account
					accountHelper.getAccount().getGroups().getGroupsList().add(result);

					// Fire an event
					eventBus.fireEvent(new UserEnteredToGroupEvent(result));
					dataCache.remove( result.getGroupIdentity().getId() );

					// Invoke handler
					handler.onSuccess(result);
				}
			}
		});
	}

	@Override
	public void leaveGroup(final String groupId, final AsyncCallback<Boolean> handler) {
		Preconditions.checkArgument(!Strings.isNullOrEmpty(groupId), "null group id");

		service.leaveGroup(groupId, new AsyncCallback<Boolean>() {
			@Override
			public void onFailure(Throwable caught) {
				handler.onFailure(caught);
			}

			@Override
			public void onSuccess(Boolean result) {
				Preconditions.checkArgument(result, "fail leave the group");
				AccountGroup g = accountHelper.getAccountGroup(groupId);

				// Remove group from local storage
				accountHelper.getAccount().getGroups().removeFromGroup(groupId);

				// Fire an event
				eventBus.fireEvent(new UserLeaveFromGroupEvent(g.getGroupIdentity()));
				handler.onSuccess(true);
			}
		});
	}

	@Override
	public void kickUserFromGroup(String groupId, String userId, AsyncCallback<Boolean> handler) {
		Preconditions.checkArgument(!Strings.isNullOrEmpty(groupId), "null group id");
		Preconditions.checkArgument(!Strings.isNullOrEmpty(userId), "null user id");
		Preconditions
				.checkArgument(accountHelper.hasPermissions(groupId, AccountPermission.CAN_KICK_USERS),
						"no permissions");

		service.kickUserFromGroup(groupId, userId, handler);
	}

	@Override
	public void getGroupQuestion(String groupId, AsyncCallback<String> handler) {
		service.getGroupTestQuestion(groupId, handler);
	}

	@Override
	public void getGroupQuestionAnswer(String id, final AsyncCallback<String> asyncCallback) {
		getGroupData(id, new AsyncCallback<GroupData>() {
			@Override
			public void onFailure(Throwable caught) {	
			}

			@Override
			public void onSuccess(GroupData result) {
				asyncCallback.onSuccess( result.getTestAnswer() );
			}
		});
	}

	@Override
	public void updateGroupQuestAnswer(final String id, final String quest, final String questAnswer,
			final AsyncCallback<Boolean> asyncCallback) {
		service.updateGroupTestQuest(id, quest, questAnswer, new AsyncCallback<Boolean>() {
			@Override
			public void onFailure(Throwable caught) {
			}

			@Override
			public void onSuccess(Boolean result) {
				GroupData data = dataCache.get(id);
				
				// Update local cache
				if(data != null)
					data.setTestQuestion(quest, questAnswer);
				
				// Invoke handler
				asyncCallback.onSuccess(true);
			}
		});
	}

	@Override
	public void getGroupMembers(String groupId, int offset, int limit,
			AsyncCallback<List<Pair<AccountIdentity, AccountGroup>>> asyncCallback) {
		service.getGroupMembers(groupId, offset, limit, asyncCallback);
	}
}
